/**
 * Copyright © 2021 IBM Corporation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
#include "mapped_floor.hpp"

#include "../manager.hpp"
#include "../zone.hpp"
#include "group.hpp"
#include "sdeventplus.hpp"

#include <nlohmann/json.hpp>
#include <phosphor-logging/lg2.hpp>

#include <algorithm>
#include <format>

namespace phosphor::fan::control::json
{

using json = nlohmann::json;

template <typename T>
uint64_t addFloorOffset(uint64_t floor, T offset, const std::string& actionName)
{
    if constexpr (!std::is_arithmetic_v<T>)
    {
        throw std::runtime_error("Invalid variant type in addFloorOffset");
    }

    auto newFloor = static_cast<T>(floor) + offset;
    if (newFloor < 0)
    {
        lg2::error(
            "{ACTION_NAME}: Floor offset of {FLOOR_OFFSET} resulted in negative floor",
            "ACTION_NAME", actionName, "FLOOR_OFFSET", offset);
        return floor;
    }

    return static_cast<uint64_t>(newFloor);
}

MappedFloor::MappedFloor(const json& jsonObj,
                         const std::vector<Group>& groups) :
    ActionBase(jsonObj, groups)
{
    setKeyGroup(jsonObj);
    setFloorTable(jsonObj);
    setDefaultFloor(jsonObj);
    setCondition(jsonObj);
}

const Group* MappedFloor::getGroup(const std::string& name)
{
    auto groupIt =
        find_if(_groups.begin(), _groups.end(),
                [name](const auto& group) { return name == group.getName(); });

    if (groupIt == _groups.end())
    {
        throw ActionParseError{
            ActionBase::getName(),
            std::format("Group name {} is not a valid group", name)};
    }

    return &(*groupIt);
}

void MappedFloor::setKeyGroup(const json& jsonObj)
{
    if (!jsonObj.contains("key_group"))
    {
        throw ActionParseError{ActionBase::getName(),
                               "Missing required 'key_group' entry"};
    }
    _keyGroup = getGroup(jsonObj["key_group"].get<std::string>());
}

void MappedFloor::setDefaultFloor(const json& jsonObj)
{
    if (jsonObj.contains("default_floor"))
    {
        _defaultFloor = jsonObj["default_floor"].get<uint64_t>();
    }
}

void MappedFloor::setFloorTable(const json& jsonObj)
{
    if (!jsonObj.contains("fan_floors"))
    {
        throw ActionParseError{ActionBase::getName(),
                               "Missing fan_floors JSON entry"};
    }

    const auto& fanFloors = jsonObj.at("fan_floors");

    for (const auto& floors : fanFloors)
    {
        if (!floors.contains("key") || !floors.contains("floors"))
        {
            throw ActionParseError{
                ActionBase::getName(),
                "Missing key or floors entries in actions/fan_floors JSON"};
        }

        FanFloors ff;
        ff.keyValue = getJsonValue(floors["key"]);

        if (floors.contains("floor_offset_parameter"))
        {
            ff.offsetParameter =
                floors["floor_offset_parameter"].get<std::string>();
        }

        if (floors.contains("default_floor"))
        {
            ff.defaultFloor = floors["default_floor"].get<uint64_t>();
        }

        for (const auto& groupEntry : floors["floors"])
        {
            if ((!groupEntry.contains("group") &&
                 !groupEntry.contains("parameter")) ||
                !groupEntry.contains("floors"))
            {
                throw ActionParseError{
                    ActionBase::getName(),
                    "Missing group, parameter, or floors entries in "
                    "actions/fan_floors/floors JSON"};
            }

            FloorGroup fg;
            if (groupEntry.contains("group"))
            {
                fg.groupOrParameter =
                    getGroup(groupEntry["group"].get<std::string>());
            }
            else
            {
                fg.groupOrParameter =
                    groupEntry["parameter"].get<std::string>();
            }

            for (const auto& floorEntry : groupEntry["floors"])
            {
                if (!floorEntry.contains("value") ||
                    !floorEntry.contains("floor"))
                {
                    throw ActionParseError{
                        ActionBase::getName(),
                        "Missing value or floor entries in "
                        "actions/fan_floors/floors/floors JSON"};
                }

                auto value = getJsonValue(floorEntry["value"]);
                auto floor = floorEntry["floor"].get<uint64_t>();

                fg.floorEntries.emplace_back(std::move(value),
                                             std::move(floor));
            }

            ff.floorGroups.push_back(std::move(fg));
        }

        _fanFloors.push_back(std::move(ff));
    }
}

void MappedFloor::setCondition(const json& jsonObj)
{
    // condition_group, condition_value, and condition_op
    // are optional, though they must show up together.
    // Assume if condition_group is present then they all
    // must be.
    if (!jsonObj.contains("condition_group"))
    {
        return;
    }

    _conditionGroup = getGroup(jsonObj["condition_group"].get<std::string>());

    if (_conditionGroup->getMembers().size() != 1)
    {
        throw ActionParseError{
            ActionBase::getName(),
            std::format("condition_group {} must only have 1 member",
                        _conditionGroup->getName())};
    }

    if (!jsonObj.contains("condition_value"))
    {
        throw ActionParseError{ActionBase::getName(),
                               "Missing required 'condition_value' entry in "
                               "mapped_floor action"};
    }

    _conditionValue = getJsonValue(jsonObj["condition_value"]);

    if (!jsonObj.contains("condition_op"))
    {
        throw ActionParseError{ActionBase::getName(),
                               "Missing required 'condition_op' entry in "
                               "mapped_floor action"};
    }

    _conditionOp = jsonObj["condition_op"].get<std::string>();

    if ((_conditionOp != "equal") && (_conditionOp != "not_equal"))
    {
        throw ActionParseError{ActionBase::getName(),
                               "Invalid 'condition_op' value in "
                               "mapped_floor action"};
    }
}

/**
 * @brief Converts the variant to a double if it's a
 *        int32_t or int64_t.
 */
void tryConvertToDouble(PropertyVariantType& value)
{
    std::visit(
        [&value](auto&& val) {
            using V = std::decay_t<decltype(val)>;
            if constexpr (std::is_same_v<int32_t, V> ||
                          std::is_same_v<int64_t, V>)
            {
                value = static_cast<double>(val);
            }
        },
        value);
}

std::optional<PropertyVariantType> MappedFloor::getMaxGroupValue(
    const Group& group)
{
    std::optional<PropertyVariantType> max;
    bool checked = false;

    for (const auto& member : group.getMembers())
    {
        try
        {
            auto value = Manager::getObjValueVariant(
                member, group.getInterface(), group.getProperty());

            // Only allow a group to have multiple members if it's numeric.
            // Unlike std::is_arithmetic, bools are not considered numeric
            // here.
            if (!checked && (group.getMembers().size() > 1))
            {
                std::visit(
                    [&group, this](auto&& val) {
                        using V = std::decay_t<decltype(val)>;
                        if constexpr (!std::is_same_v<double, V> &&
                                      !std::is_same_v<int32_t, V> &&
                                      !std::is_same_v<int64_t, V>)
                        {
                            throw std::runtime_error{std::format(
                                "{}: Group {} has more than one member but "
                                "isn't numeric",
                                ActionBase::getName(), group.getName())};
                        }
                    },
                    value);
                checked = true;
            }

            if (max && (value > max))
            {
                max = value;
            }
            else if (!max)
            {
                max = value;
            }
        }
        catch (const std::out_of_range& e)
        {
            // Property not there, continue on
        }
    }

    if (max)
    {
        tryConvertToDouble(*max);
    }

    return max;
}

bool MappedFloor::meetsCondition()
{
    if (!_conditionGroup)
    {
        return true;
    }

    bool meets = false;

    // setCondition() also checks these
    assert(_conditionGroup->getMembers().size() == 1);
    assert((_conditionOp == "equal") || (_conditionOp == "not_equal"));

    const auto& member = _conditionGroup->getMembers()[0];

    try
    {
        auto value =
            Manager::getObjValueVariant(member, _conditionGroup->getInterface(),
                                        _conditionGroup->getProperty());

        if ((_conditionOp == "equal") && (value == _conditionValue))
        {
            meets = true;
        }
        else if ((_conditionOp == "not_equal") && (value != _conditionValue))
        {
            meets = true;
        }
    }
    catch (const std::out_of_range& e)
    {
        // Property not there, so consider it failing the 'equal'
        // condition and passing the 'not_equal' condition.
        if (_conditionOp == "equal")
        {
            meets = false;
        }
        else // not_equal
        {
            meets = true;
        }
    }

    return meets;
}

void MappedFloor::run(Zone& zone)
{
    if (!meetsCondition())
    {
        // Make sure this no longer has a floor hold
        if (zone.hasFloorHold(getUniqueName()))
        {
            zone.setFloorHold(getUniqueName(), 0, false);
        }
        return;
    }

    std::optional<uint64_t> newFloor;

    auto keyValue = getMaxGroupValue(*_keyGroup);
    if (!keyValue)
    {
        auto floor = _defaultFloor ? *_defaultFloor : zone.getDefaultFloor();
        zone.setFloorHold(getUniqueName(), floor, true);
        return;
    }

    for (const auto& floorTable : _fanFloors)
    {
        // First, find the floorTable entry to use based on the key value.
        auto tableKeyValue = floorTable.keyValue;

        // Convert numeric values from the JSON to doubles so they can
        // be compared to values coming from D-Bus.
        tryConvertToDouble(tableKeyValue);

        // The key value from D-Bus must be less than the value
        // in the table for this entry to be valid.
        if (*keyValue >= tableKeyValue)
        {
            continue;
        }

        // Now check each group in the tables
        for (const auto& [groupOrParameter, floorGroups] :
             floorTable.floorGroups)
        {
            std::optional<PropertyVariantType> propertyValue;

            if (std::holds_alternative<std::string>(groupOrParameter))
            {
                propertyValue = Manager::getParameter(
                    std::get<std::string>(groupOrParameter));
                if (propertyValue)
                {
                    tryConvertToDouble(*propertyValue);
                }
                else
                {
                    // If the parameter isn't there, then don't use
                    // this floor table
                    lg2::debug(
                        "{ACTION_NAME}: Parameter {PARAMETER} specified in the JSON "
                        "could not be found",
                        "ACTION_NAME", ActionBase::getName(), "PARAMETER",
                        std::get<std::string>(groupOrParameter));
                    continue;
                }
            }
            else
            {
                propertyValue =
                    getMaxGroupValue(*std::get<const Group*>(groupOrParameter));
            }

            std::optional<uint64_t> floor;
            if (propertyValue)
            {
                // Do either a <= or an == check depending on the data type
                // to get the floor value based on this group.
                for (const auto& [tableValue, tableFloor] : floorGroups)
                {
                    PropertyVariantType value{tableValue};
                    tryConvertToDouble(value);

                    if (std::holds_alternative<double>(*propertyValue))
                    {
                        if (*propertyValue <= value)
                        {
                            floor = tableFloor;
                            break;
                        }
                    }
                    else if (*propertyValue == value)
                    {
                        floor = tableFloor;
                        break;
                    }
                }
            }

            // No floor found in this group, use a default floor for now but
            // let keep going in case it finds a higher one.
            if (!floor)
            {
                if (floorTable.defaultFloor)
                {
                    floor = *floorTable.defaultFloor;
                }
                else if (_defaultFloor)
                {
                    floor = *_defaultFloor;
                }
                else
                {
                    floor = zone.getDefaultFloor();
                }
            }

            // Keep track of the highest floor value found across all
            // entries/groups
            if ((newFloor && (floor > *newFloor)) || !newFloor)
            {
                newFloor = floor;
            }
        }

        // if still no floor, use the default one from the floor table if
        // there
        if (!newFloor && floorTable.defaultFloor)
        {
            newFloor = floorTable.defaultFloor.value();
        }

        if (newFloor)
        {
            *newFloor = applyFloorOffset(*newFloor, floorTable.offsetParameter);
        }

        // Valid key value for this entry, so done
        break;
    }

    if (!newFloor)
    {
        newFloor = _defaultFloor ? *_defaultFloor : zone.getDefaultFloor();
    }

    zone.setFloorHold(getUniqueName(), *newFloor, true);
}

uint64_t MappedFloor::applyFloorOffset(uint64_t floor,
                                       const std::string& offsetParameter) const
{
    if (!offsetParameter.empty())
    {
        auto offset = Manager::getParameter(offsetParameter);
        if (offset)
        {
            if (std::holds_alternative<int32_t>(*offset))
            {
                return addFloorOffset(floor, std::get<int32_t>(*offset),
                                      getUniqueName());
            }
            else if (std::holds_alternative<int64_t>(*offset))
            {
                return addFloorOffset(floor, std::get<int64_t>(*offset),
                                      getUniqueName());
            }
            else if (std::holds_alternative<double>(*offset))
            {
                return addFloorOffset(floor, std::get<double>(*offset),
                                      getUniqueName());
            }
            else
            {
                throw std::runtime_error(
                    "Invalid data type in floor offset parameter ");
            }
        }
    }

    return floor;
}

} // namespace phosphor::fan::control::json
